#include "TabSettings.h"

#include <QtCore>
#include <QtGui>

static const char * const groupKey = "EditorTabSettings";
static const char * const keySpacesForTabs = "SpacesForTabs";
static const char * const keyAutoSpacesForTabs = "AutoSpacesForTabs";
static const char * const keySmartBackspace = "SmartBackspace";
static const char * const keyAutoIndent = "AutoIndent";
static const char * const keyTabSize = "TabSize";
static const char * const keyIndentSize = "IndentSize";
static const char * const keyTabKeyBehavoir = "TabKeyBehavoir";

namespace CodeEdit {

TabSettings::TabSettings()
    : spacesForTabs(true)
    , autoSpacesForTabs(true)
    , autoIndent(true)
    , smartBackspace(true)
    , tabSize(4)
    , indentSize(4)
    , tabKeyBehavior(TabAlwaysIndents)
{
}

/**
 * Saves settings to storage.
 */
void TabSettings::toSettings(QSettings* s) const
{
    s->beginGroup(groupKey);
    s->setValue(QLatin1String(keySpacesForTabs), spacesForTabs);
    s->setValue(QLatin1String(keyAutoSpacesForTabs), autoSpacesForTabs);
    s->setValue(QLatin1String(keyAutoIndent), autoIndent);
    s->setValue(QLatin1String(keySmartBackspace), smartBackspace);
    s->setValue(QLatin1String(keyTabSize), tabSize);
    s->setValue(QLatin1String(keyIndentSize), indentSize);
    s->setValue(QLatin1String(keyTabKeyBehavoir), tabKeyBehavior);
    s->endGroup();
}

/**
 * Loads settings from storage.
 */
void TabSettings::fromSettings(QSettings* s)
{
    *this = TabSettings(); // assign defaults

    s->beginGroup(groupKey);
    spacesForTabs = s->value(QLatin1String(keySpacesForTabs), spacesForTabs).toBool();
    autoSpacesForTabs = s->value(QLatin1String(keyAutoSpacesForTabs), autoSpacesForTabs).toBool();
    autoIndent = s->value(QLatin1String(keyAutoIndent), autoIndent).toBool();
    smartBackspace = s->value(QLatin1String(keySmartBackspace), smartBackspace).toBool();
    tabSize = s->value(QLatin1String(keyTabSize), tabSize).toInt();
    indentSize = s->value(QLatin1String(keyIndentSize), indentSize).toInt();
    tabKeyBehavior = (TabKeyBehavior)s->value(QLatin1String(keyTabKeyBehavoir), tabKeyBehavior).toInt();
    s->endGroup();
}

/**
 * Used for handling Home key.
 */
bool TabSettings::cursorIsAtBeginingOfLine(const QTextCursor& cursor) const
{
    QString text = cursor.block().text();
    int fns = firstNonSpace(text);
    return (cursor.position() - cursor.block().position() <= fns);
}

int TabSettings::lineIndentPosition(const QString& text) const
{
    int i = 0;
    while (i < text.size()) {
        if (!text.at(i).isSpace())
            break;
        ++i;
    }
    int column = columnAt(text, i);
    return i - (column % indentSize);
}

int TabSettings::firstNonSpace(const QString& text) const
{
    int i = 0;
    while (i < text.size()) {
        if (!text.at(i).isSpace())
            return i;
        ++i;
    }
    return i;
}

/**
 * Returns all spaces from the left of the `text`, including tabs.
 */
QString TabSettings::indentationString(const QString& text) const
{
    return text.left(firstNonSpace(text));
}

int TabSettings::indentationColumn(const QString& text) const
{
    return columnAt(text, firstNonSpace(text));
}

int TabSettings::trailingWhitespaces(const QString& text) const
{
    int i = 0;
    while (i < text.size()) {
        if (!text.at(text.size()-1-i).isSpace())
            return i;
        ++i;
    }
    return i;
}

/**
 * Not clean if the indentation contains both spaces and tabs.
 */
bool TabSettings::isIndentationClean(const QTextBlock& block) const
{
    int i = 0;
    int spaceCount = 0;
    QString text = block.text();
    bool spacesForTabs = guessSpacesForTabs(block);
    while (i < text.size()) {
        QChar c = text.at(i);
        if (!c.isSpace())
            return true;
        if (c == QLatin1Char(' ')) {
            ++spaceCount;
            if (!spacesForTabs && spaceCount == tabSize)
                return false;
        }
        else if (c == QLatin1Char('\t')) {
            if (spacesForTabs || spaceCount != indentSize)
                return false;
            spaceCount = 0;
        }
        ++i;
    }
    return true;
}

/**
 * Returns whether Tab key should indent according to tabKeyBehavior setting.
 */
bool TabSettings::tabShouldIndent(const QTextDocument* document, QTextCursor cursor, int* suggestedPosition) const
{
    if (tabKeyBehavior == TabNeverIndents) {
        return false;
    }
    QTextCursor tc = cursor;
    if (suggestedPosition)
        *suggestedPosition = tc.position(); // at least suggest original position
    tc.movePosition(QTextCursor::StartOfLine);
    if (tc.atBlockEnd()) // cursor was on a blank line
        return true;
    if (document->characterAt(tc.position()).isSpace()) {
        tc.movePosition(QTextCursor::WordRight);
        if (tc.columnNumber() >= cursor.columnNumber()) {
            if (suggestedPosition)
                *suggestedPosition = tc.position(); // suggest position after whitespace
            if (tabKeyBehavior == TabLeadingWhitespaceIndents)
                return true;
        }
    }
    return tabKeyBehavior == TabAlwaysIndents;
}

int TabSettings::columnAt(const QString& text, int position) const
{
    int column = 0;
    for (int i = 0; i < position; ++i) {
        if (text.at(i) == QLatin1Char('\t'))
            column = column - (column % tabSize) + tabSize;
        else
            ++column;
    }
    return column;
}

int TabSettings::spacesLeftFromPosition(const QString& text, int position) const
{
    int i = position;
    while (i > 0) {
        if (!text.at(i-1).isSpace())
            break;
        --i;
    }
    return position - i;
}

int TabSettings::indentedColumn(int column, bool doIndent) const
{
    int aligned = (column / indentSize) * indentSize;
    if (doIndent)
        return aligned + indentSize;
    if (aligned < column)
        return aligned;
    return qMax(0, aligned - indentSize);
}

bool TabSettings::guessSpacesForTabs(const QTextBlock& block) const
{
    if (spacesForTabs && autoSpacesForTabs && block.isValid()) {
        const QTextDocument* doc = block.document();
        QVector<QTextBlock> currentBlocks(2, block); // [0] looks back; [1] looks forward
        int maxLookAround = 100;
        while (maxLookAround-- > 0) {
            if (currentBlocks.at(0).isValid())
                currentBlocks[0] = currentBlocks.at(0).previous();
            if (currentBlocks.at(1).isValid())
                currentBlocks[1] = currentBlocks.at(1).next();
            bool done = true;
            foreach (const QTextBlock& blk, currentBlocks) {
                if (blk.isValid())
                    done = false;
                if (!blk.isValid() || blk.length() == 0)
                    continue;
                const QChar firstChar = doc->characterAt(blk.position());
                if (firstChar == QLatin1Char(' ')) {
                    return true;
                }
                else if (firstChar == QLatin1Char('\t')) {
                    return false;
                }
            }
            if (done)
                break;
        }
    }
    return spacesForTabs;
}

QString TabSettings::indentationString(int startColumn, int targetColumn, const QTextBlock& block) const
{
    targetColumn = qMax(startColumn, targetColumn);
    if (guessSpacesForTabs(block))
        return QString(targetColumn - startColumn, QLatin1Char(' '));

    QString s;
    int alignedStart = startColumn - (startColumn % tabSize) + tabSize;
    if (alignedStart > startColumn && alignedStart <= targetColumn) {
        s += QLatin1Char('\t');
        startColumn = alignedStart;
    }
    if (int columns = targetColumn - startColumn) {
        int tabs = columns / tabSize;
        s += QString(tabs, QLatin1Char('\t'));
        s += QString(columns - tabs * tabSize, QLatin1Char(' '));
    }
    return s;
}

void TabSettings::indentLine(QTextBlock block, int newIndent) const
{
    const QString text = block.text();
    const int oldBlockLength = text.size();

    // quickly check whether indenting is required
    if (indentationColumn(text) == newIndent)
        return;

    const QString indentString = indentationString(0, newIndent, block);
    newIndent = indentString.length();

    if (oldBlockLength == indentString.length() && text == indentString)
        return;

    QTextCursor cursor(block);
    cursor.beginEditBlock();
    cursor.movePosition(QTextCursor::StartOfBlock);
    cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace(text));
    cursor.removeSelectedText();
    cursor.insertText(indentString);
    cursor.endEditBlock();
}

void TabSettings::reindentLine(QTextBlock block, int delta) const
{
    const QString text = block.text();
    const int oldBlockLength = text.size();

    int oldIndent = indentationColumn(text);
    int newIndent = qMax(oldIndent + delta, 0);

    if (oldIndent == newIndent)
        return;

    const QString indentString = indentationString(0, newIndent, block);
    newIndent = indentString.length();

    if (oldBlockLength == indentString.length() && text == indentString)
        return;

    QTextCursor cursor(block);
    cursor.beginEditBlock();
    cursor.movePosition(QTextCursor::StartOfBlock);
    cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace(text));
    cursor.removeSelectedText();
    cursor.insertText(indentString);
    cursor.endEditBlock();
}

bool TabSettings::equals(const TabSettings& ts) const
{
    return spacesForTabs == ts.spacesForTabs
        && autoSpacesForTabs == ts.autoSpacesForTabs
        && autoIndent == ts.autoIndent
        && smartBackspace == ts.smartBackspace
        && tabSize == ts.tabSize
        && indentSize == ts.indentSize
        && tabKeyBehavior == ts.tabKeyBehavior;
}

} // namespace CodeEdit
